/*
* AUTHOR: Kevin Lam
*/
package com.apps.services;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import com.apps.ubc.cc.model.AmazonBookModel;
public class AmazonWebService {
private static final String AWS_ACCESS_KEY_ID = "AKIAI2EIYYQQFCWV6XCA";
private static final String AWS_SECRET_KEY = "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";
private static final String ASSOCIATE_TAG_CA = "ubcoco03-20";
private static final String ECS_CA = "ecs.amazonaws.ca";
private static final int ISBN_LOOKUP = 0x1;
private static final int PRICE_LOOKUP = 0x2;
private static final int IMAGE_LOOKUP = 0x3;
private static final int FETCH_ASIN = 0x1;
private static final int FETCH_TITLE = 0x2;
private static final int FETCH_DETAIL = 0x3;
private static final int FETCH_USED_PRICE = 0x4;
private static final int FETCH_NEW_PRICE = 0x5;
private static final int FETCH_IMAGE = 0x6;
private SignedRequestsHelper helper;
public AmazonWebService(){
try {
helper = SignedRequestsHelper.getInstance(ECS_CA,
AWS_ACCESS_KEY_ID, AWS_SECRET_KEY);
} catch (Exception e) {
e.printStackTrace();
}
}
public AmazonBookModel search(String keyword) {
String requestURL = lookUpKeyword(keyword);
String title = fetch(requestURL, FETCH_TITLE);
String detail = fetch(requestURL, FETCH_DETAIL);
String asin = fetch(requestURL, FETCH_ASIN);
requestURL = lookUpPrice(asin,PRICE_LOOKUP);
String priceNew = fetch(requestURL, FETCH_NEW_PRICE);
String priceUsed = fetch(requestURL, FETCH_USED_PRICE);
requestURL = lookUpImage(asin);
String imageUrl = fetch(requestURL, FETCH_IMAGE);
AmazonBookModel abm = new AmazonBookModel(asin, title, imageUrl,
detail, priceNew, priceUsed);
return abm;
}
private String lookUpKeyword(String keywords) {
String requestUrl = null;
Map<String, String> params = getLookupMap(ISBN_LOOKUP);
params.put("Keywords", keywords);
if (helper != null)
requestUrl = helper.sign(params);
return requestUrl;
}
private String lookUpPrice(String asin, int type) {
String requestUrl = null;
Map<String, String> params = getLookupMap(type);
params.put("ItemId", asin);
if (helper != null)
requestUrl = helper.sign(params);
return requestUrl;
}
private String lookUpImage(String asin) {
String requestUrl = null;
Map<String, String> params = getLookupMap(IMAGE_LOOKUP);
params.put("ItemId", asin);
if (helper != null)
requestUrl = helper.sign(params);
return requestUrl;
}
private String fetch(String requestUrl, int type) {
String result = "";
if(requestUrl == null || requestUrl.equals(""))
return "";
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(requestUrl);
Node node = null;
switch (type) {
case FETCH_ASIN:
node = doc.getElementsByTagName("ASIN").item(0);
break;
case FETCH_DETAIL:
node = doc.getElementsByTagName("DetailPageURL").item(0);
break;
case FETCH_IMAGE:
Node parentNode = doc.getElementsByTagName("SmallImage")
.item(0);
if (parentNode != null)
node = parentNode.getChildNodes().item(0);
break;
case FETCH_NEW_PRICE:
Node lowestNewPriceNode = doc.getElementsByTagName(
"LowestNewPrice").item(0);
if (lowestNewPriceNode != null)
node = lowestNewPriceNode.getLastChild();
break;
case FETCH_USED_PRICE:
Node offerListingIdNode = doc.getElementsByTagName(
"OfferListingId").item(0);
if (offerListingIdNode != null)
node = offerListingIdNode.getNextSibling().getLastChild();
break;
case FETCH_TITLE:
node = doc.getElementsByTagName("Title").item(0);
break;
}
if (node != null)
result = node.getTextContent();
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
private HashMap<String, String> getLookupMap(int mapEnum) {
HashMap<String, String> params = new HashMap<String, String>();
params.put("AssociateTag", ASSOCIATE_TAG_CA);
params.put("Service", "AWSECommerceService");
params.put("Version", "2009-03-31");
switch (mapEnum) {
case ISBN_LOOKUP:
params.put("IdType", "ISBN");
params.put("SearchIndex", "Books");
params.put("Operation", "ItemSearch");
break;
case PRICE_LOOKUP:
params.put("IdType", "ASIN");
params.put("Operation", "ItemLookup");
params.put("ResponseGroup", "Offers");
params.put("Condition", "Used");
break;
case IMAGE_LOOKUP:
params.put("IdType", "ASIN");
params.put("Operation", "ItemLookup");
params.put("ResponseGroup", "Images");
params.put("Condition", "All");
break;
}
return params;
}
// SignedRequestHelper taken from Amazon documentation
static class SignedRequestsHelper {
/**
* All strings are handled as UTF-8
*/
private static final String UTF8_CHARSET = "UTF-8";
/**
* The HMAC algorithm required by Amazon
*/
private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
/**
* This is the URI for the service, don't change unless you really know
* what you're doing.
*/
private static final String REQUEST_URI = "/onca/xml";
/**
* The sample uses HTTP GET to fetch the response. If you changed the
* sample to use HTTP POST instead, change the value below to POST.
*/
private static final String REQUEST_METHOD = "GET";
private String endpoint = null;
private String awsAccessKeyId = null;
private String awsSecretKey = null;
private SecretKeySpec secretKeySpec = null;
private Mac mac = null;
/**
* You must provide the three values below to initialize the helper.
*
* @param endpoint
* Destination for the requests.
* @param awsAccessKeyId
* Your AWS Access Key ID
* @param awsSecretKey
* Your AWS Secret Key
*/
public static SignedRequestsHelper getInstance(String endpoint,
String awsAccessKeyId, String awsSecretKey)
throws IllegalArgumentException, UnsupportedEncodingException,
NoSuchAlgorithmException, InvalidKeyException {
if (null == endpoint || endpoint.length() == 0) {
throw new IllegalArgumentException("endpoint is null or empty");
}
if (null == awsAccessKeyId || awsAccessKeyId.length() == 0) {
throw new IllegalArgumentException(
"awsAccessKeyId is null or empty");
}
if (null == awsSecretKey || awsSecretKey.length() == 0) {
throw new IllegalArgumentException(
"awsSecretKey is null or empty");
}
SignedRequestsHelper instance = new SignedRequestsHelper();
instance.endpoint = endpoint.toLowerCase();
instance.awsAccessKeyId = awsAccessKeyId;
instance.awsSecretKey = awsSecretKey;
byte[] secretyKeyBytes = instance.awsSecretKey
.getBytes(UTF8_CHARSET);
instance.secretKeySpec = new SecretKeySpec(secretyKeyBytes,
HMAC_SHA256_ALGORITHM);
instance.mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
instance.mac.init(instance.secretKeySpec);
return instance;
}
/**
* The construct is private since we'd rather use getInstance()
*/
private SignedRequestsHelper() {
}
/**
* This method signs requests in hashmap form. It returns a URL that
* should be used to fetch the response. The URL returned should not be
* modified in any way, doing so will invalidate the signature and
* Amazon will reject the request.
*/
public String sign(Map<String, String> params) {
// Let's add the AWSAccessKeyId and Timestamp parameters to the
// request.
params.put("AWSAccessKeyId", this.awsAccessKeyId);
params.put("Timestamp", this.timestamp());
// The parameters need to be processed in lexicographical order, so
// we'll
// use a TreeMap implementation for that.
SortedMap<String, String> sortedParamMap = new TreeMap<String, String>(
params);
// get the canonical form the query string
String canonicalQS = this.canonicalize(sortedParamMap);
// create the string upon which the signature is calculated
String toSign = REQUEST_METHOD + "\n" + this.endpoint + "\n"
+ REQUEST_URI + "\n" + canonicalQS;
// get the signature
String hmac = this.hmac(toSign);
String sig = this.percentEncodeRfc3986(hmac);
// construct the URL
String url = "http://" + this.endpoint + REQUEST_URI + "?"
+ canonicalQS + "&Signature=" + sig;
return url;
}
/**
* This method signs requests in query-string form. It returns a URL
* that should be used to fetch the response. The URL returned should
* not be modified in any way, doing so will invalidate the signature
* and Amazon will reject the request.
*/
public String sign(String queryString) {
// let's break the query string into it's constituent name-value
// pairs
Map<String, String> params = this.createParameterMap(queryString);
// then we can sign the request as before
return this.sign(params);
}
/**
* Compute the HMAC.
*
* @param stringToSign
* String to compute the HMAC over.
* @return base64-encoded hmac value.
*/
private String hmac(String stringToSign) {
String signature = null;
byte[] data;
byte[] rawHmac;
try {
data = stringToSign.getBytes(UTF8_CHARSET);
rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();
signature = new String(encoder.encode(rawHmac));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
}
return signature;
}
/**
* Generate a ISO-8601 format timestamp as required by Amazon.
*
* @return ISO-8601 format timestamp.
*/
private String timestamp() {
String timestamp = null;
Calendar cal = Calendar.getInstance();
DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
timestamp = dfm.format(cal.getTime());
return timestamp;
}
/**
* Canonicalize the query string as required by Amazon.
*
* @param sortedParamMap
* Parameter name-value pairs in lexicographical order.
* @return Canonical form of query string.
*/
private String canonicalize(SortedMap<String, String> sortedParamMap) {
if (sortedParamMap.isEmpty()) {
return "";
}
StringBuffer buffer = new StringBuffer();
Iterator<Map.Entry<String, String>> iter = sortedParamMap
.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, String> kvpair = iter.next();
buffer.append(percentEncodeRfc3986(kvpair.getKey()));
buffer.append("=");
buffer.append(percentEncodeRfc3986(kvpair.getValue()));
if (iter.hasNext()) {
buffer.append("&");
}
}
String cannoical = buffer.toString();
return cannoical;
}
/**
* Percent-encode values according the RFC 3986. The built-in Java
* URLEncoder does not encode according to the RFC, so we make the extra
* replacements.
*
* @param s
* decoded string
* @return encoded string per RFC 3986
*/
private String percentEncodeRfc3986(String s) {
String out;
try {
out = URLEncoder.encode(s, UTF8_CHARSET).replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
out = s;
}
return out;
}
/**
* Takes a query string, separates the constituent name-value pairs and
* stores them in a hashmap.
*
* @param queryString
* @return
*/
private Map<String, String> createParameterMap(String queryString) {
Map<String, String> map = new HashMap<String, String>();
String[] pairs = queryString.split("&");
for (String pair : pairs) {
if (pair.length() < 1) {
continue;
}
String[] tokens = pair.split("=", 2);
for (int j = 0; j < tokens.length; j++) {
try {
tokens[j] = URLDecoder.decode(tokens[j], UTF8_CHARSET);
} catch (UnsupportedEncodingException e) {
}
}
switch (tokens.length) {
case 1: {
if (pair.charAt(0) == '=') {
map.put("", tokens[0]);
} else {
map.put(tokens[0], "");
}
break;
}
case 2: {
map.put(tokens[0], tokens[1]);
break;
}
}
}
return map;
}
}
}